import copy
import json
import multiprocessing
import os
import platform
import re
import subprocess
import sys

class StartMinecraft:
    def __init__(self, interface) -> None:
        self.interface = interface
        self.base_path = ""
        self.option = {}
        self.name = ""
        self.java_path = "java"
    
    def check_file(self):
        downloader = self.interface("Setup.DownloadClient")
        downloader.getJson()
        downloader.install_assets(downloader.version_json, self.base_path, True)

    def start(self):
        self.check_file()
        game_ps = multiprocessing.Process(target=self.run)
        game_ps.start()

    def run(self):
        start_cmd = self.get_minecraft_command(self.name, self.base_path, self.option)
        start_cmd[0] = self.java_path
        game = subprocess.Popen(start_cmd, stdout=open("log.log", "w", encoding="utf-8"), stderr=open("err.log", "w", encoding="utf-8"))
        exitcode = game.wait()
        if exitcode:
            print(exitcode)

    def inherit_json(self, original_data, path: str):
        """
        Implement the inheritsFrom function
        See https://github.com/tomsik68/mclauncher-api/wiki/Version-Inheritance-&-Forge
        """
        inherit_version = original_data["inheritsFrom"]
        with open(os.path.join(path, "versions", inherit_version, inherit_version + ".json")) as f:
            new_data = json.load(f)
        for key, value in original_data.items():
            if isinstance(value, list) and isinstance(new_data.get(key, None), list):
                new_data[key] = value + new_data[key]
            elif isinstance(value, dict) and isinstance(new_data.get(key, None), dict):
                for a, b in value.items():
                    if isinstance(b, list):
                        new_data[key][a] = new_data[key][a] + b
            else:
                new_data[key] = value
        return new_data
    
    def get_library_path(self, name: str, path: str) -> str:
        """
        Returns the path from a libname
        """
        libpath = os.path.join(path, "libraries")
        parts = name.split(":")
        base_path, libname, version = parts[0:3]
        for i in base_path.split("."):
            libpath = os.path.join(libpath, i)
        try:
            version, fileend = version.split("@")
        except ValueError:
            fileend = "jar"

        # construct a filename with the remaining parts
        filename = f"{libname}-{version}{''.join(map(lambda p: f'-{p}', parts[3:]))}.{fileend}"
        libpath = os.path.join(libpath, libname, version, filename)
        return libpath
    
    def get_natives(self, data) -> str:
        """
        Returns the native part from the json data
        """
        if platform.architecture()[0] == "32bit":
            arch_type = "32"
        else:
            arch_type = "64"
        if "natives" in data:
            if platform.system() == 'Windows':
                if "windows" in data["natives"]:
                    return data["natives"]["windows"].replace("${arch}", arch_type)
                else:
                    return ""
            elif platform.system() == 'Darwin':
                if "osx" in data["natives"]:
                    return data["natives"]["osx"].replace("${arch}", arch_type)
                else:
                    return ""
            else:
                if "linux" in data["natives"]:
                    return data["natives"]["linux"].replace("${arch}", arch_type)
                else:
                    return ""
        else:
            return ""
    
    def get_libraries(self, name, data, path: str) -> str:
        """
        Returns the argument with all libs that come after -cp
        """
        classpath_seperator = self.get_classpath_separator()
        libstr = ""
        for i in data["libraries"]:
            if not self.parse_rule_list(i, "rules", {}):
                continue
            libstr += self.get_library_path(i["name"], path) + classpath_seperator
            native = self.get_natives(i)
            if native != "":
                if "downloads" in i:
                    libstr += os.path.join(path, "libraries", i["downloads"]["classifiers"][native]["path"]) + classpath_seperator
                else:
                    libstr += self.get_library_path(i["name"] + "-" + native, path) + classpath_seperator
        return libstr + os.path.join(path, "versions", name, name + ".jar")

    def get_os_version(self) -> str:
        """
        Try to implement System.getProperty("os.version") from Java for use in rules
        This doesn't work on mac yet
        """
        if platform.system() == "Windows":
            ver = sys.getwindowsversion()
            return f"{ver.major}.{ver.minor}"
        elif platform.system == "Darwin":
            return ""
        else:
            return platform.uname().release

    def parse_single_rule(self, rule, options) -> bool:
        """
        Parse a single rule from the versions.json
        """
        if rule["action"] == "allow":
            returnvalue = False
        elif rule["action"] == "disallow":
            returnvalue = True
        if "os" in rule:
            for key, value in rule["os"].items():
                if key == "name":
                    if value == "windows" and platform.system() != 'Windows':
                        return returnvalue
                    elif value == "osx" and platform.system() != 'Darwin':
                        return returnvalue
                    elif value == "linux" and platform.system() != 'Linux':
                        return returnvalue
                elif key == "arch":
                    if value == "x86" and platform.architecture()[0] != "32bit":
                        return returnvalue
                elif key == "version":
                    if not re.match(value, self.get_os_version()):
                        return returnvalue
        if "features" in rule:
            for key, value in rule["features"].items():
                if key == "has_custom_resolution" and not options.get("customResolution", False):
                    return returnvalue
                elif key == "is_demo_user" and not options.get("demo", False):
                    return returnvalue
        return not returnvalue
    
    def parse_rule_list(self, data, rule_string: str, options) -> bool:
        """
        Parse a list of rules
        """
        if rule_string not in data:
            return True
        for i in data[rule_string]:
            if not self.parse_single_rule(i, options):
                return False
        return True
    
    def get_classpath_separator(self) -> str:
        """
        Returns the classpath seperator for the current os
        """
        if platform.system() == "Windows":
            return ";"
        else:
            return ":"
    
    def _get_jvm_platform_string(self) -> str:
        """
        Get the name that is used the identify the platform
        """
        if platform.system() == "Windows":
            if platform.architecture()[0] == "32bit":
                return "windows-x86"
            else:
                return "windows-x64"
        elif platform.system() == "Linux":
            if platform.architecture()[0] == "32bit":
                return "linux-i386"
            else:
                return "linux"
        elif platform.system() == "Darwin":
            if platform.machine() == "arm64":
                return "mac-os-arm64"
            else:
                return "mac-os"
        else:
            return "gamecore"
    
    def get_executable_path(self, jvm_version: str, minecraft_directory):
        """
        Returns the path to the executable. Returns None if none is found.
        """
        java_path = os.path.join(minecraft_directory, "runtime", jvm_version, self._get_jvm_platform_string(), jvm_version, "bin", "java")
        if os.path.isfile(java_path):
            return java_path
        elif os.path.isfile(java_path + ".exe"):
            return java_path + ".exe"
        java_path = java_path.replace(os.path.join("bin", "java"), os.path.join("jre.bundle", "Contents", "Home", "bin", "java"))
        if os.path.isfile(java_path):
            return java_path
        else:
            return None
    
    def get_launcher_version(self):
        return self.interface("ConstantInfo.ConstantInfo").CORE_VERSION
    
    def replace_arguments(self, argstr: str, versionData, path: str, options) -> str:
        """
        Replace all placeholder in arguments with the needed value
        """
        argstr = argstr.replace("${natives_directory}", options["nativesDirectory"])
        argstr = argstr.replace("${launcher_name}", options.get("launcherName", "minecraft-launcher-lib"))
        argstr = argstr.replace("${launcher_version}", options.get("launcherVersion", self.get_launcher_version()))
        argstr = argstr.replace("${classpath}", options["classpath"])
        argstr = argstr.replace("${auth_player_name}", options.get("username", "{username}"))
        argstr = argstr.replace("${version_name}", versionData["id"])
        argstr = argstr.replace("${game_directory}", options.get("gameDirectory", path))
        argstr = argstr.replace("${assets_root}", os.path.join(path, "assets"))
        argstr = argstr.replace("${assets_index_name}", versionData.get("assets", versionData["id"]))
        argstr = argstr.replace("${auth_uuid}", options.get("uuid", "{uuid}"))
        argstr = argstr.replace("${auth_access_token}", options.get("token", "{token}"))
        argstr = argstr.replace("${user_type}", "mojang")
        argstr = argstr.replace("${version_type}", versionData["type"])
        argstr = argstr.replace("${user_properties}", "{}")
        argstr = argstr.replace("${resolution_width}", options.get("resolutionWidth", "854"))
        argstr = argstr.replace("${resolution_height}", options.get("resolutionHeight", "480"))
        argstr = argstr.replace("${game_assets}", os.path.join(path, "assets", "virtual", "legacy"))
        argstr = argstr.replace("${auth_session}", options.get("token", "{token}"))
        argstr = argstr.replace("${library_directory}", os.path.join(path, "libraries"))
        argstr = argstr.replace("${classpath_separator}", self.get_classpath_separator())
        return argstr
    
    def get_arguments(self, data, versionData, path: str, options):
        """
        Returns all arguments from the version.json
        """
        arglist = []
        for i in data:
            # Rules might has 2 different names in different versions.json
            if not self.parse_rule_list(i, "compatibilityRules", options):
                continue
            if not self.parse_rule_list(i, "rules", options):
                continue
            # i could be the argument
            if isinstance(i, str):
                arglist.append(self.replace_arguments(i, versionData, path, options))
            else:
                # Sometimes  i["value"] is the argument
                if isinstance(i["value"], str):
                    arglist.append(self.replace_arguments(i["value"], versionData, path, options))
                # Sometimes i["value"] is a list of arguments
                else:
                    for v in i["value"]:
                        v = self.replace_arguments(v, versionData, path, options)
                        arglist.append(v)
        return arglist
    
    def get_arguments_string(self, versionData, path: str, options):
        """
        Turns the argument string from the version.json into a list
        """
        arglist = []
        for v in versionData["minecraftArguments"].split(" "):
            v = self.replace_arguments(v, versionData, path, options)
            arglist.append(v)
        # Custom resolution is not in the list
        if options.get("customResolution", False):
            arglist.append("--width")
            arglist.append(options.get("resolutionWidth", "854"))
            arglist.append("--height")
            arglist.append(options.get("resolutionHeight", "480"))
        if options.get("demo", False):
            arglist.append("--demo")
        return arglist

    def get_minecraft_command(self, version: str, path: str, options):
        """
        Returns a command for launching Minecraft. For more information take a look at the documentation.
        """
        if not os.path.isdir(os.path.join(path, "versions", version)):
            print("version notfound")
        options = copy.copy(options)
        with open(os.path.join(path, "versions", version, version + ".json")) as f:
            data = json.load(f)
        if "inheritsFrom" in data:
            data = self.inherit_json(data, path)
        options["nativesDirectory"] = options.get("nativesDirectory", os.path.join(path, "versions", version, "natives"))
        options["classpath"] = self.get_libraries(version, data, path)
        command = []
        # Add Java executable
        if "executablePath" in options:
            command.append(options["executablePath"])
        elif "javaVersion" in data:
            java_path = self.get_executable_path(data["javaVersion"]["component"], path)
            if java_path is None:
                command.append("java")
            else:
                command.append(java_path)
        else:
            command.append(options.get("defaultExecutablePath", "java"))
        if "jvmArguments" in options:
            command = command + options["jvmArguments"]
        # Newer Versions have jvmArguments in version.json
        if isinstance(data.get("arguments", None), dict):
            if "jvm" in data["arguments"]:
                command = command + self.get_arguments(data["arguments"]["jvm"], data, path, options)
            else:
                command.append("-Djava.library.path=" + options["nativesDirectory"])
                command.append("-cp")
                command.append(options["classpath"])
        else:
            command.append("-Djava.library.path=" + options["nativesDirectory"])
            command.append("-cp")
            command.append(options["classpath"])
        # The argument for the logger file
        if options.get("enableLoggingConfig", False):
            if "logging" in data:
                if len(data["logging"]) != 0:
                    logger_file = os.path.join(path, "assets", "log_configs", data["logging"]["client"]["file"]["id"])
                    command.append(data["logging"]["client"]["argument"].replace("${path}", logger_file))
        command.append(data["mainClass"])
        if "minecraftArguments" in data:
            # For older versions
            command = command + self.get_arguments_string(data, path, options)
        else:
            command = command + self.get_arguments(data["arguments"]["game"], data, path, options)
        if "server" in options:
            command.append("--server")
            command.append(options["server"])
            if "port" in options:
                command.append("--port")
                command.append(options["port"])
        if options.get("disableMultiplayer", False):
            command.append("--disableMultiplayer")
        if options.get("disableChat", False):
            command.append("--disableChat")
        return command
        
class StartMinecraftServer:
    def __init__(self, interface) -> None:
        self.interface = interface
        self.java_path = "java"
        self.name = ""
        self.base_path = ""
        self.option = {}
        self.error_log = []

    def check_eula(self):
        with open("eula.txt", "r", encoding="utf-8") as eula:
            if eula.readlines()[-1].strip() != "eula=true":
                eula.seek(0)
                print("You need to agree to the EULA in order to run the server. Go to eula.txt for more info.")
                print("|-------------------------|")
                print(f"| {eula.readlines()[-1].strip()} -> eula=true |")
                print("|-------------------------|")
                os.startfile("eula.txt")
    
    def error_classifiers(self) -> bool:
        if len(self.error_log) and self.error_log[0].strip() == "Error: A JNI error has occurred, please check your installation and try again":
            print("Please, check your JavaRuntime version.")
            r = re.match("Exception in thread (\".*\") (.*): .*version (.*)\).*to (.*)", self.error_log[1].strip())
            print("need version: ", r.group(3), " or later.\n", "but only have: ", r.group(4), sep="")
            return False
        return True

    def start(self):
        os.chdir(os.path.join(os.getcwd(), self.base_path, "server_versions", self.name))
        server_ps = multiprocessing.Process(target=self.run)
        server_ps.start()

    def run(self):
        start_cmd = self.get_server_command(self.name, self.option)
        start_cmd[0] = self.java_path
        game = subprocess.Popen(start_cmd, stdout=open("log.log", "w", encoding="utf-8"), stderr=open("err.log", "w", encoding="utf-8"))
        exitcode = game.wait()
        if exitcode:
            if "bdas" in self.option.keys() and self.option["bdas"]:
                with open("err.log", "r", encoding="utf-8") as f:
                    print("Server exit with exitcode:", exitcode)
                    self.error_log = f.readlines()
                if self.error_classifiers():
                    self.run()
            else:
                with open("err.log", "r", encoding="utf-8") as f:
                    print("Server exit with exitcode:", exitcode)
                    self.error_log = f.readlines()
                    self.error_classifiers()
        else:
            self.check_eula()
            print("Server exit with exitcode:", exitcode)

    def get_server_command(self, name, options):
        jvm = self.get_jvm_args(options)
        return [self.java_path] + jvm + ["-jar", name + ".jar"]
    
    def get_jvm_args(self, options):
        jvm = []
        if "memoryMax" in options.keys():
            jvm.append("-Xmx" + options["memoryMax"])
        if "memoryMin" in options.keys():
            jvm.append("-Xms" + options["memoryMin"])
        if "gui" in options.keys() and not options["gui"]:
            jvm.append("")
        return jvm